Highlight graph changes per rewrite step in proof view #190#448
Highlight graph changes per rewrite step in proof view #190#448dorakingx wants to merge 21 commits intozxcalc:masterfrom
Conversation
Made-with: Cursor
|
Thanks for the PR, Screen.Recording.2026-02-26.at.17.47.29.mov |
Made-with: Cursor
Made-with: Cursor
|
Thanks for the detailed feedback and the video! It was incredibly helpful.
Note on the temporary layout overlap (Drag-and-Drop): I have attached a new video showing the corrected highlighting behavior across different operations. Could you please take another look when you have time? Screen.Recording.2026-02-28.at.3.32.50.mov |
|
First, we would like the rewrites to highlight the next change in each proof step, rather than the previous. This is because the person doing the proof knows what they've just changed, but looking back at the proof it's helpful to see what was changed between proof steps. Instead of highlighting all edges of spiders involved in the latest rewrite rule, the intended behavior is to only highlighted the subset of those spiders' edges that were changed. For example, for spider fusion, the only edge highlighted should be the edge between the two spiders being fused, not their other edges. Lastly, it should be possible to toggle the highlighting on and off in the GUI settings. |
…dent_edges - Add highlight_match_pairs to Rewrite/AddRewriteStep for MATCH_DOUBLE rules - In move_to_step, when match pairs exist, find fusion edge via incident_edges(v1) instead of g.edges(v1,v2) - Pass match pairs from rewrite_action (tree) and proof_panel (drag-drop) for fuse - Add highlight toggle in settings; update test for next-change semantics Made-with: Cursor
…tices - Add highlight_unfuse_verts to Rewrite and AddRewriteStep for unfuse steps - Unfuse branch: highlight vertices from highlight_unfuse_verts that exist in current graph - When both split vertices exist (viewing after unfuse), highlight edge between them - Avoid coordinate-based path for unfuse so extra vertices at same coords are not highlighted Made-with: Cursor
|
Thanks again for the excellent feedback and the survey! The guidance on forward-looking highlights and precise edge selection was incredibly helpful and greatly improved the feature. I have pushed a new update that fully implements your requested design changes, along with a final fix for the Magic Wand tool. Here is a summary of the latest updates:
I have attached a short video/screenshots demonstrating the refined highlighting across manual operations, drag-and-drop, the Magic Wand, and the new toggle setting in action. Could you please take another look when you have a chance? Let me know if there's anything else you'd like me to adjust! Screen.Recording.2026-02-28.at.10.10.58.mov |
|
Thanks, it looks better. Before we go ahead and review this further, could you please make your PR as minimal as possible and it changes only what's necessary? Currently it has over 800 new lines and I am not sure if all of those are necessary. Such big PRs are hard to review especially since we are reaching the end of the hackathon. |
… comp/remove id; optimize (merge unfuse into highlight_verts, add _edges_between) Made-with: Cursor
|
Thanks for the feedback. I've reduced the PR so it only includes what's needed for the feature. What I changed:
Current size: The branch is now ~455 insertions and ~61 deletions vs If you’d like it even smaller, I can trim further (e.g. fewer comments or a more minimal test). Tell me which parts you’d prefer to see reduced and I’ll adjust. |
|
Thanks, the size looks more reasonable now. Will have a look at it soon |
|
@dorakingx I am still finding it difficult to understand the changes you have made in the code. Can you explain and summarize what you did? And the lint check is failing right now; can you please fix that? |
…nds.py, type ignore setColorScheme in app.py Made-with: Cursor
|
@RazinShaikh Here is the summary. And I fixed the lint check error. Summary of changesWhat this PR doesThis PR adds forward-looking rewrite highlighting in proof mode: when you select a proof step, the graph highlights what will change in the next step (the vertices and edges affected by that rewrite), instead of what changed in the previous one. It also adds a GUI toggle to turn this highlighting on or off, and ensures only the relevant vertices/edges are highlighted (no coordinate drift or unrelated nodes). 1. Highlight behaviour
So the logic is: “when this step was created, we recorded which vertices (and for fuse, which pair) were involved; when we show this step, we highlight those in the current graph.” 2. Where metadata is set
3. GUI toggle
4. Lint fixes (mypy)
Files touched (overview)
|
RazinShaikh
left a comment
There was a problem hiding this comment.
- MatchType can be single, double or compound. Your code does not deal with compound. You can look at rewrite_data.py to see which rewrites are compound.
- The highlights do not work for custom rewrites
custom-rule.mp4
- The highlights should look pretty. It should have better colours and it should look good in light mode, dark mode and all the color schemes.
zxlive/proof.py
Outdated
| def _edges_between(g: GraphT, v1: int, v2: int) -> set[ET]: | ||
| """Return the set of edges between v1 and v2 in g.""" | ||
| out: set[ET] = set() | ||
| for e in g.incident_edges(v1): | ||
| s, t = g.edge_st(e) | ||
| if (s == v1 and t == v2) or (s == v2 and t == v1): | ||
| out.add(e) | ||
| return out | ||
|
|
There was a problem hiding this comment.
Isn't something like this already in PyZX?
There was a problem hiding this comment.
You’re right, PyZX already provides this functionality.
BaseGraph.edges(s, t) returns exactly the edges between two vertices, so my original implementation was effectively re‑implementing that logic. I’ve updated _edges_between to be a thin wrapper around g.edges(v1, v2) instead of manually iterating over incident_edges, and kept the helper only to make the intent at the call site (“edges between v1 and v2”) explicit.
There was a problem hiding this comment.
You don't need a wrapper around the PyZX method. Please just use it directly and delete this method.
There was a problem hiding this comment.
I changed it to use PyZX's methods directly.
| """Helper method to apply animation and push the unfuse command to the undo stack. | ||
| """ | ||
| def _finalize_unfuse( | ||
| self, v: VT, new_g: GraphT, extra_vertices: Optional[list[VT]] = None |
There was a problem hiding this comment.
what is extra vertices and what does it do?
There was a problem hiding this comment.
extra_vertices is an optional list of additional vertices created during the unfuse operation (for example, the new W-input/W-output pair in _unfuse_w).
_finalize_unfuse always highlights the original vertex v, and, if extra_vertices is provided, it appends those vertex IDs so that all vertices involved in the unfuse are included in highlight_verts:
verts = [v] + (extra_vertices or [])
cmd = AddRewriteStep(
self.graph_view, new_g, self.step_view, "unfuse",
highlight_verts=verts,
)So extra_vertices doesn’t affect the rewrite logic itself; it only controls which newly created vertices are highlighted together with v in the proof step UI.
| if self._it is None and self.scene is not None and self.v is not None: | ||
| self._it = self.scene.vertex_map[self.v] | ||
| assert self._it is not None | ||
| self._it = self.scene.vertex_map.get(self.v) | ||
| if self._it is None: | ||
| raise RuntimeError(f"VItemAnimation: missing target item for vertex {self.v}") | ||
| return self._it | ||
|
|
There was a problem hiding this comment.
What are all the changes in this file starting from line 430 to the end of file for? What's their purpose?
There was a problem hiding this comment.
Those changes all live in the VItemAnimation / PhaseItem area and were made for robustness and UI clarity, not to change the semantics of the underlying graph.
Concretely:
-
VItemAnimation.itproperty (lines ~426–432)- Lazily resolves the
VItemfromscene.vertex_mapusingget(self.v)and raises aRuntimeErrorif the vertex has disappeared. - This avoids
KeyError/AttributeErrorwhen an animation is still referenced but the corresponding vertex was removed by a rewrite.
- Lazily resolves the
-
_on_state_changed(lines ~434–459)- Wraps access to
self.itin atry/exceptand cleanly stops the animation if the target item no longer exists, instead of crashing. - Maintains the
active_animationsset on the targetVItem:- When starting, it stops any other animations on the same property before adding itself.
- When stopping, it uses
discardso we don’t getKeyErrorif we’re already gone.
- This guarantees that at most one animation per property is active and prevents stale animations from lingering.
- Wraps access to
-
updateCurrentValue(lines ~460–481)- Early‑returns if the animation is not in
Runningstate. - Again guards
self.itwithtry/exceptand stops quietly if the target item vanished. - Applies the interpolated value to
Position/Scale/Rectand callstarget.refresh()when appropriate. - This is the core of making vertex animations safe even when the underlying graph is being mutated by rewrites.
- Early‑returns if the animation is not in
-
PhaseItemchanges (lines ~484–509)- Adjusts the phase‑label text color based on dark mode (
display_setting.dark_mode) so that phases remain readable on both light and dark themes. - The rest of
PhaseItem.refreshis unchanged; it still renders the phase label above the vertex.
- Adjusts the phase‑label text color based on dark mode (
-
rotate_pointandget_w_partner_vitemhelpers (lines ~511–526)- Small utility functions used by other code paths (e.g. W‑node unfuse) to rotate points and to safely look up the
VItemcorresponding to a W‑partner vertex, returningNoneinstead of throwing if it’s missing.
- Small utility functions used by other code paths (e.g. W‑node unfuse) to rotate points and to safely look up the
In summary, everything from ~430 to the end of the file is about:
- making vertex animations resilient when rewrites delete or replace vertices, and
- improving the visual appearance (especially phase labels) across light/dark color schemes,
without changing the underlying graph semantics.
There was a problem hiding this comment.
The explanations you gave here have nothing to do with highlighting diffs which is what this PR is about. Please undo these irrelevant changes
There was a problem hiding this comment.
I've restored it to its original state.
…iable in vitem.py Made-with: Cursor
…from matches_list Made-with: Cursor
- Custom rewrite highlights: use last_rewrite_verts for post-apply vertex IDs - Add identity: forward-looking highlight_edge_pairs for magic wand edge - Add identity last-step fix reverted (forward-looking semantics) - highlight_edge_pairs for edge-only highlighting - Scheme-aware highlight colors (vertex/edge) in settings - Fixed colors #FFB74D/#FFC107 for all modes (light/dark, all schemes) Made-with: Cursor
|
Made-with: Cursor
|
For custom rewrite, you can create any rewrite and use it but it won't highlight the changes. This is because you need to handle the custom rewrite case when the add rewrites is called with highlight verts |
Made-with: Cursor
Made-with: Cursor
…nflicts Made-with: Cursor
|
@RazinShaikh If you could share the exact test.zxg phase-gadget-fusion.zxr Screen.Recording.2026-03-03.at.0.48.59.mov |
|
Here's the phase gadget fusion rule file. But note that no custom rewrites work in general. Also, I get the impression that instead of generically adding highlight verts, etc. based on match_single, match_double and match_compound, you have actually hard-coded this for certain rules. As such, the rules that you have not hard-coded do not highlight. For instance, the remove self loops rule does not highlight among many. |
Made-with: Cursor
Made-with: Cursor
Made-with: Cursor
|
I’ve now implemented highlighting in a generic way for all custom rules, not just for specific ones. As you can see in the video, this works both for:
In both cases the selected LHS region is correctly highlighted after the rewrite is applied, confirming that the new logic handles arbitrary custom rules as intended. Screen.Recording.2026-03-03.at.15.20.43.mov |
Made-with: Cursor
|
Thanks for participating in the hackathon. We liked the implementation you proposed for this PR, but unfortunately there are still certain issues that need to be resolved before merging. In particular, edges are not highlighted when a rewrite only affects edges (e.g. self-loop removal, removing parallel edges). Also when we save a proof and reopen it, the highlights are lost, which needs to be addressed somehow (e.g. by saving the highlights as well). |
Made-with: Cursor
|
I tried self-loop removal and removing parallel edges, but as shown in the video, the highlights appear to be correct. Also, in my environment, the highlights are preserved even when I save the proof and reopen it. Please verify this. Screen.Recording.2026-03-04.at.0.02.52.mov |
|
Sorry for the late reply. I will check this later today. |
This PR introduces visual highlighting of nodes and edges that change between consecutive proof steps, making it easier to see what each rewrite did.
Rewrite diff highlighting
GraphDiffbetween that step’s graph and the previous one (ProofStepView.move_to_stepinproof.py).new_verts,changed_vertex_types,changed_phases,changed_vdata,changed_pos) and edges (new_edges,changed_edata,changed_edge_types) are collected and passed to the scene as “highlight sets”.Rendering changes
GraphScenenow maintains sets of highlighted vertices and edges and exposes:set_rewrite_highlight(verts, edges)clear_rewrite_highlight()is_vertex_highlighted(v)/is_edge_highlighted(e)VItem.refreshuses these flags to draw highlighted vertices with a thicker, accent-colored outline (different colors for light/dark mode).EItem.refreshsimilarly draws highlighted edges with a thicker, accent-colored pen while preserving existing styling (e.g. Hadamard dash patterns).Behavior
START) clears all highlighting.You can verify the behavior by opening a proof, applying a few rewrite steps, and moving up and down the step list: only the nodes and edges affected by the selected rewrite should be emphasized.